﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using OpenGauss.NET.Internal;
using OpenGauss.NET.Internal.TypeHandlers;
using OpenGauss.NET.Internal.TypeHandling;
using OpenGauss.NET.PostgresTypes;
using OpenGauss.NET.Replication.PgOutput.Messages;
using OpenGauss.NET.TypeMapping;
using OpenGauss.NET.Util;

namespace OpenGauss.NET.BackendMessages
{
    /// <summary>
    /// A RowDescription message sent from the backend.
    /// </summary>
    /// <remarks>
    /// See https://www.postgresql.org/docs/current/static/protocol-message-formats.html
    /// </remarks>
    sealed class RowDescriptionMessage : IBackendMessage, IReadOnlyList<FieldDescription>
    {
        FieldDescription?[] _fields;
        readonly Dictionary<string, int> _nameIndex;
        Dictionary<string, int>? _insensitiveIndex;

        internal RowDescriptionMessage(int numFields = 10)
        {
            _fields = new FieldDescription[numFields];
            _nameIndex = new Dictionary<string, int>();
        }

        RowDescriptionMessage(RowDescriptionMessage source)
        {
            Count = source.Count;
            _fields = new FieldDescription?[Count];
            for (var i = 0; i < Count; i++)
                _fields[i] = source._fields[i]!.Clone();
            _nameIndex = new Dictionary<string, int>(source._nameIndex);
            if (source._insensitiveIndex?.Count > 0)
                _insensitiveIndex = new Dictionary<string, int>(source._insensitiveIndex);
        }

        internal RowDescriptionMessage Load(OpenGaussReadBuffer buf, ConnectorTypeMapper typeMapper)
        {
            _nameIndex.Clear();
            _insensitiveIndex?.Clear();

            var numFields = Count = buf.ReadInt16();
            if (_fields.Length < numFields)
            {
                var oldFields = _fields;
                _fields = new FieldDescription[numFields];
                Array.Copy(oldFields, _fields, oldFields.Length);
            }

            for (var i = 0; i < numFields; ++i)
            {
                var field = _fields[i] ??= new();

                field.Populate(
                    typeMapper,
                    name:                  buf.ReadNullTerminatedString(),
                    tableOID:              buf.ReadUInt32(),
                    columnAttributeNumber: buf.ReadInt16(),
                    oid:                   buf.ReadUInt32(),
                    typeSize:              buf.ReadInt16(),
                    typeModifier:          buf.ReadInt32(),
                    formatCode:            (FormatCode)buf.ReadInt16()
                );

                if (!_nameIndex.ContainsKey(field.Name))
                    _nameIndex.Add(field.Name, i);
            }

            return this;
        }

        internal static RowDescriptionMessage CreateForReplication(
            ConnectorTypeMapper typeMapper, uint tableOID, FormatCode formatCode, IReadOnlyList<RelationMessage.Column> columns)
        {
            var msg = new RowDescriptionMessage(columns.Count);
            var numFields = msg.Count = columns.Count;

            for (var i = 0; i < numFields; ++i)
            {
                var field = msg._fields[i] = new();
                var column = columns[i];

                field.Populate(
                    typeMapper,
                    name:                  column.ColumnName,
                    tableOID:              tableOID,
                    columnAttributeNumber: checked((short)i),
                    oid:                   column.DataTypeId,
                    typeSize:              0, // TODO: Confirm we don't have this in replication
                    typeModifier:          column.TypeModifier,
                    formatCode:            formatCode
                );

                if (!msg._nameIndex.ContainsKey(field.Name))
                    msg._nameIndex.Add(field.Name, i);
            }

            return msg;
        }

        public FieldDescription this[int index]
        {
            get
            {
                Debug.Assert(index < Count);
                Debug.Assert(_fields[index] != null);

                return _fields[index]!;
            }
        }

        public int Count { get; private set; }

        public IEnumerator<FieldDescription> GetEnumerator() => new Enumerator(this);
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

        /// <summary>
        /// Given a string name, returns the field's ordinal index in the row.
        /// </summary>
        internal int GetFieldIndex(string name)
            => TryGetFieldIndex(name, out var ret)
                ? ret
                : throw new IndexOutOfRangeException("Field not found in row: " + name);

        /// <summary>
        /// Given a string name, returns the field's ordinal index in the row.
        /// </summary>
        internal bool TryGetFieldIndex(string name, out int fieldIndex)
        {
            if (_nameIndex.TryGetValue(name, out fieldIndex))
                return true;

            if (_insensitiveIndex is null || _insensitiveIndex.Count == 0)
            {
                if (_insensitiveIndex == null)
                    _insensitiveIndex = new Dictionary<string, int>(InsensitiveComparer.Instance);

                foreach (var kv in _nameIndex)
                    if (!_insensitiveIndex.ContainsKey(kv.Key))
                        _insensitiveIndex[kv.Key] = kv.Value;
            }

            return _insensitiveIndex.TryGetValue(name, out fieldIndex);
        }

        public BackendMessageCode Code => BackendMessageCode.RowDescription;

        internal RowDescriptionMessage Clone() => new(this);

        /// <summary>
        /// Comparer that's case-insensitive and Kana width-insensitive
        /// </summary>
        sealed class InsensitiveComparer : IEqualityComparer<string>
        {
            public static readonly InsensitiveComparer Instance = new();
            static readonly CompareInfo CompareInfo = CultureInfo.InvariantCulture.CompareInfo;

            InsensitiveComparer() {}

            // We should really have CompareOptions.IgnoreKanaType here, but see
            // https://github.com/dotnet/corefx/issues/12518#issuecomment-389658716
            public bool Equals(string? x, string? y)
                => CompareInfo.Compare(x, y, CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType) == 0;

            public int GetHashCode(string o)
                => CompareInfo.GetSortKey(o, CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType).GetHashCode();
        }

        class Enumerator : IEnumerator<FieldDescription>
        {
            readonly RowDescriptionMessage _rowDescription;
            int _pos = -1;

            public Enumerator(RowDescriptionMessage rowDescription)
                => _rowDescription = rowDescription;

            public FieldDescription Current
                => _pos >= 0 ? _rowDescription[_pos] : throw new InvalidOperationException();

            object IEnumerator.Current => Current;

            public bool MoveNext()
            {
                if (_pos == _rowDescription.Count - 1)
                    return false;
                _pos++;
                return true;
            }

            public void Reset() => _pos = -1;
            public void Dispose() {}
        }
    }

    /// <summary>
    /// A descriptive record on a single field received from PostgreSQL.
    /// See RowDescription in https://www.postgresql.org/docs/current/static/protocol-message-formats.html
    /// </summary>
    public sealed class FieldDescription
    {
#pragma warning disable CS8618  // Lazy-initialized type
        internal FieldDescription() {}

        internal FieldDescription(uint oid)
            : this("?", 0, 0, oid, 0, 0, FormatCode.Binary) {}

        internal FieldDescription(
            string name, uint tableOID, short columnAttributeNumber,
            uint oid, short typeSize, int typeModifier, FormatCode formatCode)
        {
            Name = name;
            TableOID = tableOID;
            ColumnAttributeNumber = columnAttributeNumber;
            TypeOID = oid;
            TypeSize = typeSize;
            TypeModifier = typeModifier;
            FormatCode = formatCode;
        }
#pragma warning restore CS8618

        internal FieldDescription(FieldDescription source)
        {
            _typeMapper = source._typeMapper;
            Name = source.Name;
            TableOID = source.TableOID;
            ColumnAttributeNumber = source.ColumnAttributeNumber;
            TypeOID = source.TypeOID;
            TypeSize = source.TypeSize;
            TypeModifier = source.TypeModifier;
            FormatCode = source.FormatCode;
            Handler = source.Handler;
        }

        internal void Populate(
            ConnectorTypeMapper typeMapper, string name, uint tableOID, short columnAttributeNumber,
            uint oid, short typeSize, int typeModifier, FormatCode formatCode
        )
        {
            _typeMapper = typeMapper;
            Name = name;
            TableOID = tableOID;
            ColumnAttributeNumber = columnAttributeNumber;
            TypeOID = oid;
            TypeSize = typeSize;
            TypeModifier = typeModifier;
            FormatCode = formatCode;

            ResolveHandler();
        }

        /// <summary>
        /// The field name.
        /// </summary>
        internal string Name { get; set; }

        /// <summary>
        /// The object ID of the field's data type.
        /// </summary>
        internal uint TypeOID { get; set; }

        /// <summary>
        /// The data type size (see pg_type.typlen). Note that negative values denote variable-width types.
        /// </summary>
        public short TypeSize { get; set; }

        /// <summary>
        /// The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific.
        /// </summary>
        public int TypeModifier { get; set; }

        /// <summary>
        /// If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero.
        /// </summary>
        internal uint TableOID { get; set; }

        /// <summary>
        /// If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero.
        /// </summary>
        internal short ColumnAttributeNumber { get; set; }

        /// <summary>
        /// The format code being used for the field.
        /// Currently will be zero (text) or one (binary).
        /// In a RowDescription returned from the statement variant of Describe, the format code is not yet known and will always be zero.
        /// </summary>
        internal FormatCode FormatCode { get; set; }

        internal string TypeDisplayName => PostgresType.GetDisplayNameWithFacets(TypeModifier);

        /// <summary>
        /// The OpenGauss type handler assigned to handle this field.
        /// Returns <see cref="UnknownTypeHandler"/> for fields with format text.
        /// </summary>
        internal OpenGaussTypeHandler Handler { get; private set; }

        internal PostgresType PostgresType
            => _typeMapper.DatabaseInfo.ByOID.TryGetValue(TypeOID, out var postgresType)
                ? postgresType
                : UnknownBackendType.Instance;

        internal Type FieldType => Handler.GetFieldType(this);

        internal void ResolveHandler()
            => Handler = IsBinaryFormat ? _typeMapper.ResolveByOID(TypeOID) : _typeMapper.UnrecognizedTypeHandler;

        ConnectorTypeMapper _typeMapper;

        internal bool IsBinaryFormat => FormatCode == FormatCode.Binary;
        internal bool IsTextFormat => FormatCode == FormatCode.Text;

        internal FieldDescription Clone()
        {
            var field =  new FieldDescription(this);
            field.ResolveHandler();
            return field;
        }

        /// <summary>
        /// Returns a string that represents the current object.
        /// </summary>
        public override string ToString() => Name + (Handler == null ? "" : $"({Handler.PgDisplayName})");
    }
}
